// License Notification
//======================================================
// Copyright (c) 2010 Channel Intelligence Inc.
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// which accompanies this distribution, and is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// ======================================================




using System;
using System.Collections.Generic;
using System.Xml;
using CIP4.Common;

namespace CIP4.RulesEngine
{
    public partial class LogicalExpressionType : BooleanExpressionType
    {
        public override string XPathString(LoggerDelegate logger)
        {
            string text;
            if (ExpressionOperator == LogicalOperatorsEnum.AND || ExpressionOperator == LogicalOperatorsEnum.OR)
            {
                text = Operands[0] is BooleanExpressionType ? ((BooleanExpressionType)Operands[0]).XPathString(logger) :
                    Operands[0] is LogicalExpressionType ? ((LogicalExpressionType)Operands[0]).XPathString(logger) : string.Empty;

                text += string.Format(" {0} ", ExpressionOperator);

                text += Operands[1] is BooleanExpressionType ? ((BooleanExpressionType)Operands[1]).XPathString(logger) :
                    Operands[1] is LogicalExpressionType ? ((LogicalExpressionType)Operands[1]).XPathString(logger) : string.Empty;
            }
            else if (ExpressionOperator == LogicalOperatorsEnum.NOT)
            {
                text = Operands[0] is BooleanExpressionType ? ((BooleanExpressionType)Operands[0]).XPathString(logger) :
                    Operands[0] is LogicalExpressionType ? ((LogicalExpressionType)Operands[0]).XPathString(logger) : string.Empty;
                if (!string.IsNullOrEmpty(text))
                    text = " NOT(" + text + ")"; 
            }
            else
                throw new ApplicationException("Unexpected logical operator type");
            return text;
        }
    }

    public partial class BooleanExpressionType : ExpressionType, IBooleanValue
    {
        enum QueryFormsEnum
        {
            SqlSVForm,
            XPathSVForm,
            //XPathMVForm,
            //XPathMVReferenceForm
        }

        public virtual string XPathString(LoggerDelegate logger)
        {
            if (LiteralOp() == null)
            {
                logger(MajorEnum.Error, MinorEnum.UnsupportedXpathExpression, "The boolean expression cannot be transformed into an xpath expression. It must have both a literal and a named value: {0}", new object[] { ToString() });
                return string.Empty;
            }
            if (NamedValueOp() == null)
            {
                logger(MajorEnum.Error, MinorEnum.UnsupportedXpathExpression, "The boolean expression cannot be transformed into an xpath expression: {0}. It must have both a literal and a named value", new object[] { ToString() });
                return string.Empty;
            }
            if (Operands.Count > 2 && !(Operands[2].IsLiteral || Operands[2].IsNamedValue))
            {
                logger(MajorEnum.Error, MinorEnum.UnsupportedXpathExpression, "The boolean expression cannot be transformed into an xpath expression: {0}", new object[] { ToString() });
                return string.Empty;
            }

            if (NamedValueOp().AtomName == "ExternalID")
            {
                return string.Format(
                    QueryForms[(int)QueryFormsEnum.SqlSVForm],
                    NamedValueOp().AtomName,
                    SqlOperatorForm(ExpressionOperator, LiteralOp() == Operands[0], logger),
                    LiteralOp().XPathString(ExpressionOperator, logger));
            }
            
            return string.Format(
                QueryForms[(int)QueryFormsEnum.XPathSVForm],
                XPathName(NamedValueOp().AtomName),
                SqlOperatorForm(ExpressionOperator, LiteralOp() == Operands[0], logger),
                LiteralOp().XPathString(ExpressionOperator, logger),
                Operands.Count > 2 ? ((AtomType)Operands[0]).XPathString(ExpressionOperator, logger) : string.Empty);
        }

        static readonly List<string> QueryForms = new List<string>{
            // 0=path, 1=operator, 2=value
                " {0} {1} {2} ",
            // SVForm =
                " ObjectText.value(" +
                "   'declare namespace cii=\"" + Globals.ciiNS + "\";" + Environment.NewLine +
                "   declare namespace cio=\"" + Globals.cioNS + "\";"  + Environment.NewLine + 
                "   ({0})[1]', 'nvarchar(max)') {1} {2}",     

            // MVForm = 
                 " {2} {1} SOME (" + Environment.NewLine +
                 "  select T2.Loc.query('.').value('.', 'nvarchar(max)')" + Environment.NewLine +
                 "  from Item I2" + Environment.NewLine +
                 "  CROSS APPLY I2.ObjectText.nodes('" + Environment.NewLine +
                 "  declare namespace cii=\"" + Globals.ciiNS + "\";" + Environment.NewLine +
                 "  declare namespace cio=\"" + Globals.cioNS + "\";" + Environment.NewLine +
                 "  {0}') AS T2(Loc) " + Environment.NewLine +
                 "  WHERE Item.PublishedID = I2.PublishedID)",                                                        

            //MVReferenceForm =
                 " {2} {1} SOME (" + Environment.NewLine +
                 "  select LOWER(T2.Loc.query('.').value('.', 'nvarchar(max)'))" + Environment.NewLine +
                 "  from Item I2" + Environment.NewLine +
                 "  CROSS APPLY I2.ObjectText.nodes('" + Environment.NewLine +
                 "  declare namespace cii=\"" + Globals.ciiNS + "\";" + Environment.NewLine +
                 "  declare namespace cio=\"" + Globals.cioNS + "\";" + Environment.NewLine +
                 "  {0}') AS T2(Loc) " + Environment.NewLine +
                 "  WHERE Item.PublishedID = I2.PublishedID)"
        };

        private static string XPathName(string propertyPath)
        {
            return "cii:Item/*/cio:" + propertyPath.Substring(propertyPath.IndexOf('.') + 1).Replace(".", "/*/cio:") + "/.";
        }

        private AtomType LiteralOp()
        {
            if (Operands.Count > 0 && Operands[0].IsLiteral)
                return (AtomType)Operands[0];
            if (Operands.Count > 1 && Operands[1].IsLiteral)
                return (AtomType)Operands[1];
            return null;
        }

        private AtomType NamedValueOp()
        {
            if (Operands.Count > 0 && Operands[0].IsNamedValue)
                return (AtomType)Operands[0];
            if (Operands.Count > 1 && Operands[1].IsNamedValue)
                return (AtomType)Operands[1];
            return null;
        }

        private static string SqlOperatorForm(BooleanOperatorsEnum op, bool reverseOps, LoggerDelegate logger)
        {
            switch (op)
            {
                case BooleanOperatorsEnum.EQL:
                    return "=";
                case BooleanOperatorsEnum.NEQ:
                    return "<>";
                case BooleanOperatorsEnum.GTR:
                    if (reverseOps)
                        return "<=";
                    return ">";
                case BooleanOperatorsEnum.GEQ:
                    if (reverseOps)
                        return "<";
                    return ">=";
                case BooleanOperatorsEnum.LSS:
                    if (reverseOps)
                        return ">=";
                    return "<";
                case BooleanOperatorsEnum.LEQ:
                    if (reverseOps)
                        return ">";
                    return "<=";
                case BooleanOperatorsEnum.STARTSWITH:
                    if (reverseOps)
                        logger(MajorEnum.Error, MinorEnum.UnsupportedXpathExpression, "For the boolean LIKE operator {0} the literal must be thge second term", new object[] { op.ToString() });
                    return "LIKE";
                case BooleanOperatorsEnum.CONTAINS:
                    if (reverseOps)
                        logger(MajorEnum.Error, MinorEnum.UnsupportedXpathExpression, "For the boolean LIKE operator {0} the literal must be thge second term", new object[] { op.ToString() });
                    return "LIKE";
                default:
                    {
                        logger(MajorEnum.Error, MinorEnum.UnsupportedXpathExpression, "The boolean operator {0} is not supported", new object[] { op.ToString() });
                        return string.Empty;
                    }
            }
        }
    }

    public abstract partial class AtomType : CIValueType
    {
        public string XPathString(BooleanOperatorsEnum op, LoggerDelegate logger)
        {
            string text = string.Empty;
            switch (op)
            {
                case BooleanOperatorsEnum.EQL:
                case BooleanOperatorsEnum.NEQ:
                    if (this is BooleanAtomType)
                        text = "'" + Convert.ToBoolean(ToString()).ToString().ToLower() + "'";
                    else if (this is DateTimeAtomType)
                        text = "'" + XmlConvert.ToString(DateTime.Parse(ToString()), Globals.DateTimeSerializationMode ) + "'";
                    else if (this is StringAtomType)
                        text = "'" + ToString().Trim(new[] { '"' }) + "'";
                    else 
                        text = ToString();
                    break;
                case BooleanOperatorsEnum.GTR:
                case BooleanOperatorsEnum.GEQ:
                case BooleanOperatorsEnum.LSS:
                case BooleanOperatorsEnum.LEQ:
                    if (this is BooleanAtomType)
                        logger(MajorEnum.Error, MinorEnum.UnsupportedXpathExpression, "You cannot use operator {0} with value {1}", new object[] {op, ToString() });
                    else if (this is DateTimeAtomType ||
                        this is StringAtomType)
                        text = "'" + ToString().Trim(new[] { '"' }) + "'";
                    else
                        text = ToString();
                    break;
                case BooleanOperatorsEnum.STARTSWITH:
                    if (!(this is StringAtomType ||
                        this is DateTimeAtomType))
                        logger(MajorEnum.Error, MinorEnum.UnsupportedXpathExpression, "You cannot use operator {0} with value {1}", new object[] { op, ToString() });
                    else 
                        text = "'" + EscapeSqlLikeLiteral(ToString().Trim(new[] { '"' })) + "%' ESCAPE '!' ";
                    break;
                case BooleanOperatorsEnum.CONTAINS:
                    if (!(this is StringAtomType ||
                        this is DateTimeAtomType))
                        logger(MajorEnum.Error, MinorEnum.UnsupportedXpathExpression, "You cannot use operator {0} with value {1}", new object[] { op, ToString() });
                    else
                        text = "'%" + EscapeSqlLikeLiteral(ToString().Trim(new[] { '"' })) + "%' ESCAPE '!' ";
                    break;
                default:
                    logger(MajorEnum.Error, MinorEnum.UnsupportedXpathExpression, "Unsupported operator: {0}", new object[] { op.ToString() });
                    break;
            }
            return text;
        }

        /// <summary>
        /// Sql LIKE seems to be broken - but anyway - we do our best - see 
        /// 
        ///     http://msdn.microsoft.com/en-us/library/aa933232(SQL.80).aspx
        /// 
        /// For a discussion of escaping for Sql LIKE - reserved characters are []%~_
        /// These can be escaped by declaring an escape literal after the like clause using 
        /// 
        ///     ESCAPE '!'
        ///     
        /// Should be able to escape the escape by putting it in [] - but that doesn't seem to work
        /// As a poor man's alternative, could replace ! with % - but could geta  much broader query
        /// result as a consequence - on balance seems easiest to penalize people for putting ! in 
        /// their data. Put it in as [!] and report it as a Sql bug!
        /// </summary>
        /// <param name="s"></param>
        /// <returns></returns>
        private static string EscapeSqlLikeLiteral(string s)
        {
            return s.Replace("!", "[!]").Replace("[", "![").Replace("]", "!]").Replace("_", "!_").Replace("%", "!%").Replace("^", "!^");
        }


    }
}